Skip to content

Thread tanh logit softcapping through FlashAttention (FA2, opt-in FA3) - #3391

Open
nvegesna-netizen wants to merge 11 commits into
NVIDIA:mainfrom
nvegesna-netizen:nvegesna/gemma2-softcap-core
Open

Thread tanh logit softcapping through FlashAttention (FA2, opt-in FA3)#3391
nvegesna-netizen wants to merge 11 commits into
NVIDIA:mainfrom
nvegesna-netizen:nvegesna/gemma2-softcap-core

Conversation

@nvegesna-netizen

@nvegesna-netizen nvegesna-netizen commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Adds a softcap kwarg to DotProductAttention so models with attention-logit soft-capping (cap·tanh(x/cap), e.g. Gemma2) can run on TE's fused flash-attention kernels instead of falling back to an unfused/non-TE path.

Companion PRs (needed together for an end-to-end model to pick this up):

  • Megatron-LM: exposes TransformerConfig.attn_logit_softcapping and maps it to this softcap kwarg
  • Megatron-Bridge: adds an opt-in Gemma2 attention path that sets attn_logit_softcapping

What changed

  • DotProductAttention (init + forward) and AttentionParams gain a softcap: float = 0.0 kwarg. softcap=0.0 is a no-op — existing behavior is unchanged.
  • Threaded through the FA2 non-CP path and all three context-parallel autograd functions (forward + ctx-saved backward).
  • UnfusedDotProductAttention applies cap * tanh(scores / cap) to the already-scaled logits, so it serves as the in-tree reference implementation (and the numerical reference the tests compare the flash backends against). With qk-layer-scaling the layer_number factor is divided out of the cap, since that path defers scaling to the softmax.
  • get_attention_backend: when softcap != 0, FusedAttention (cuDNN), FA4, and FA3 (unless the checks below pass) are disqualified and selection steers to FA2 or unfused (also disqualifies FA2 builds too old to carry the softcap kernel). This is a deliberate safety net — softcap must never be silently dropped, nor hit a NotImplementedError at runtime.
  • FA3 (Hopper only): gated on a build-capability probe (softcap present in both FA3 entry points), max(head_dim_qk, head_dim_v) <= 256, and non-CP; forward threads softcap into FA3's kwargs, backward is handled automatically by the existing Hopper autograd function. No new env var — FA3 eligibility is governed by the existing NVTE_FLASH_ATTN_V3 (default 1). Since TE already prefers FA3 over FA2 on sm90, FA3 is the default softcap backend on Hopper when a softcap-capable FA3 build is installed; that is intentional. NVTE_FLASH_ATTN_V3=0 steers to FA2.
  • Tests: three tests in tests/pytorch/attention/test_attention.py. test_dpa_softcap sweeps the available backends through the existing test_dot_product_attention harness (forward + backward parity against the unfused reference); softcap always disqualifies FusedAttention, so it opts out of the harness's fused-unavailable fallback to keep the dQ/dK/dV comparison. test_dpa_softcap_zero_backend_selection asserts softcap=0.0 leaves FusedAttention selectable and a nonzero cap does not. test_dpa_softcap_vs_reference compares forward and dQ/dK/dV against a closed-form pure-PyTorch oracle one backend at a time, so UnfusedDotProductAttention stays covered on machines without flash-attn; it uses its own randn inputs because the shared harness's 0.1 * randn puts logits at O(1e-2), where a cap of 50 moves the output by ~1e-8 and a dropped cap would be undetectable.

Supported configurations

  • FA2, non-CP — supported with flash-attn >= 2.6.0 (first FA2 release exposing a softcap kwarg). The default path wherever FA3 is not eligible (i.e. off sm90, or no softcap-capable FA3 build). Requires zero attention dropout while training — see below.
  • FA2 + context parallelism — supported for all four cp_comm_type values: p2p, all_gather, a2a, a2a+p2p (p2p and a2a+p2p share one autograd function). Same flash-attn >= 2.6.0 requirement; forward and backward both carry softcap.
  • FA3, non-CP — requires an FA3 build whose flash_attn_func/flash_attn_varlen_func both expose softcap (signature probe, fail-closed) and max(head_dim_qk, head_dim_v) <= 256. FA3 is Hopper (sm90)-only upstream and governed by the existing NVTE_FLASH_ATTN_V3 (default 1). When eligible it takes precedence over FA2 on Hopper — deliberate; both paths were exercised on Hopper (see Validation). Any check failing steers to FA2 (>= 2.6.0) or unfused.
  • UnfusedDotProductAttention, non-CP — supported; the reference/fallback path, used when no flash backend is eligible (no version floor). Unfused attention does not support context parallelism at all, independent of softcap.

Not supported:

  • FusedAttention (cuDNN) — disqualified in get_attention_backend when softcap != 0.0, so selection steers to FA2 (or unfused) rather than silently dropping the cap.
  • FA3 + CP — raises NotImplementedError (use FA2).
  • FA4 — no FA4 kernel can carry the cap, so FA4 is disqualified in get_attention_backend when softcap != 0.0; without that it would be selected on SM100 (NVTE_FLASH_ATTN_V4 defaults to 1) and silently drop the cap. See the follow-up section below.
  • FA2 + nonzero attention dropout, while training — flash-attn rejects a nonzero softcap combined with nonzero dropout at dispatch ("Softcapping does not support dropout for now", csrc/flash_attn/flash_api.cpp), so FA2 is disqualified and selection steers to unfused. Dropout reaches the kernel as 0.0 in eval, so inference configs are unaffected. Since unfused does not support CP, CP + softcap + dropout while training has no eligible backend and raises rather than crashing inside flash-attn.

ONNX export force-selects the unfused backend, which now honors softcap via torch.tanh (exportable as the ONNX Tanh op), so export is expected to work — but there is no ONNX softcap test in this PR.

softcap = 0.0 (the default) disables softcapping: backend selection and numerics are identical to today.

Validation

  • Unit: test_dpa_softcap — forward and backward parity against the unfused reference, across whichever backends are available on the test machine.
  • Unit: test_dpa_softcap_vs_reference — forward and dQ/dK/dV against a closed-form reference, for softcap in {0.0, 0.5}, with logits at O(1) so tanh runs in its saturating region. softcap=0.0 compares against a reference that never applies tanh, and the nonzero case asserts the cap moves the reference output by more than the comparison tolerance, so the test cannot pass an implementation that drops the cap.
  • Unit: test_dpa_softcap_zero_backend_selection — the softcap=0.0 no-op claim for backend selection, which is the half the filter actually changed.
  • End-to-end: exercised via the companion Megatron-LM/Megatron-Bridge changes above — multi-step training runs on both a Hopper and a Blackwell target, confirmed numerically consistent with the prior unfused path, and confirmed via kernel-level profiling that the fused flash kernels (not the fallback path) were selected at runtime.

On the Hopper target both the FA2 and the FA3 softcap paths were exercised. FA3 taking precedence over FA2 there is an intentional design choice.

Follow-up, not in this PR: FA4

FA4 softcap support is deliberately excluded here rather than included as dead scaffolding. On Blackwell (SM100), FA4's dedicated head_dim=256 forward kernel has no score_mod/softcap fusion logic in it at all — the kernel constructor asserts score_mod is None. So there's currently no FA4 kernel path capable of serving this shape; adding an opt-in flag now would just be inert code with nothing to opt into. This follows as its own PR (stacked on this branch) once — or if — an FA4 kernel with softcap fusion for head_dim=256 lands.

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds tanh attention-logit softcapping to DotProductAttention, propagating it through supported FlashAttention and unfused execution paths.

  • Adds capability-aware selection for FA2, FA3, FA4, cuDNN, dropout, and context-parallel configurations.
  • Threads softcap through context-parallel forward and backward operations.
  • Adds backend-selection and numerical forward/backward coverage to the existing attention test suite.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
transformer_engine/pytorch/attention/dot_product_attention/utils.py Adds softcap-aware capability filtering, including the context-parallel FA3 exclusion requested by the earlier review.
transformer_engine/pytorch/attention/dot_product_attention/backends.py Implements unfused softcapping, probes FA3 support, and propagates the cap into eligible FlashAttention calls.
transformer_engine/pytorch/attention/dot_product_attention/context_parallel.py Carries softcap through FA2 context-parallel forward state and matching backward calls.
transformer_engine/pytorch/attention/dot_product_attention/dot_product_attention.py Exposes the public softcap option and consistently forwards the resolved value through selection and dispatch.
tests/pytorch/attention/test_attention.py Adds softcap backend-selection and numerical gradient tests to a module exercised by the normal L0 and L3 suites.
tests/pytorch/utils.py Extends shared attention test configuration and backend queries with the softcap value.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[DotProductAttention request] --> S{softcap nonzero?}
  S -- No --> N[Existing backend selection]
  S -- Yes --> F{Supported backend}
  F -->|FA3 capable, non-CP, head dim <= 256| FA3[FlashAttention 3]
  F -->|FA2 >= 2.6 and supported dropout mode| FA2[FlashAttention 2]
  F -->|No eligible flash backend and non-CP| U[Unfused attention]
  FA3 --> O[Softcapped attention output]
  FA2 --> O
  U --> O
Loading

Reviews (13): Last reviewed commit: "Merge branch 'main' into nvegesna/gemma2..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py
Comment thread tests/pytorch/attention/test_softcap.py Outdated
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from a6a793b to 5917f0d Compare August 17, 2026 21:21
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from 19a21eb to 5ae46ce Compare August 17, 2026 21:35
Comment thread tests/pytorch/attention/test_softcap.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py Outdated
Comment thread transformer_engine/pytorch/attention/dot_product_attention/utils.py
@cyanguwa cyanguwa added the 2.20 label Aug 27, 2026
@cyanguwa

Copy link
Copy Markdown
Collaborator

Please follow this link to fix the DCO of this PR as well. Thanks!

https://github.com/NVIDIA/TransformerEngine/pull/3391/checks?check_run_id=97902991433

nvegesna-netizen and others added 7 commits August 27, 2026 09:57
…in FA3)

Add a user `softcap` value (tanh logit softcapping, `softcap*tanh(x/softcap)`)
to DotProductAttention so models like Gemma2 can run on the fused flash path
instead of an unfused/FlexAttention kernel.

- Add `softcap` to DotProductAttention (init+forward) and AttentionParams;
  thread it into the FA2 non-CP kwargs and all three context-parallel autograd
  functions (forward + ctx-saved backward). softcap=0.0 reproduces prior behavior.
- get_attention_backend: when softcap != 0, disable FusedAttention/unfused and
  steer to FA2 -- disable FA3/FA4, and disable FA2 < 2.6.0 -- so the cap is never
  silently dropped (FA2 < 2.6.0) or hit at runtime as NotImplementedError (FA3/FA4).
  Also disable FA3 under context parallelism (its CP path hard-rejects nonzero
  softcap) so CP+softcap steers to FA2, which supports it, instead of crashing.
- FA3 softcap opt-in: NVTE_FA3_SOFTCAP=1, Hopper (sm90) hd<=256, non-CP only,
  gated on a fail-closed signature probe (fa3_supports_softcap). Forward threads
  softcap into fa_3_optional_forward_kwargs; the existing Hopper autograd function
  carries it into backward automatically. Default off; unchanged behavior steers
  to FA2.
- ONNX export: fail loudly (assert) rather than silently drop softcap -- export
  unconditionally force-selects UnfusedDotProductAttention, which has no softcap
  support, so this previously exported models with softcapping silently omitted.
- Tests: test_softcap.py (FA2 fwd/bwd parity vs pure-PyTorch reference), wired
  into qa/L0_pytorch_unittest/test.sh.

FA4 softcap opt-in is deliberately NOT included here -- see follow-up PR. On
Blackwell (SM100), FA4's dedicated head_dim=256 forward kernel has no score_mod
support at all (kernel constructor asserts `score_mod is None`), so there is
currently no FA4 kernel path this could opt into; adding the scaffolding now
would just be inert code with nothing to exercise.

Addresses review findings: CP+FA3 softcap selection crash, ONNX silent drop,
and the missing CI wiring for test_softcap.py.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
python -O / PYTHONOPTIMIZE strips assert statements, which would silently
reopen the ONNX export softcap-drop bug the previous commit fixed (ONNX mode
would again force-select UnfusedDotProductAttention with softcap silently
omitted, with no error). Switch to an explicit if/raise ValueError, which
survives optimized execution.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
for more information, see https://pre-commit.ci

(reapplied after a force-push rebase clobbered pre-commit.ci's original
19a21eb commit; same content, restored by hand)

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
…fold test into test_attention.py

UnfusedDotProductAttention now applies softcap * tanh(scores / softcap) to the
already-scaled logits, matching how FlashAttention folds softmax_scale into its
tanh argument, so it can serve as the softcap reference backend. Backend
selection therefore no longer disqualifies unfused attention for softcap, and
the ONNX-export guard is dropped since the export path force-selects unfused
and torch.tanh is exportable.

test_softcap.py is replaced by a model_configs_softcap dict and test_dpa_softcap
in test_attention.py, which reuses test_dot_product_attention for backend
sweeping.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Drop the redundant NVTE_FA3_SOFTCAP opt-in. `use_flash_attention_3` already
derives from NVTE_FLASH_ATTN_V3, so the existing flag governs the FA3 softcap
path and NVTE_FLASH_ATTN_V3=0 disables it. Correctness stays established by the
build-capability probe, head_dim <= 256, and the non-CP requirement.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
FA4 exposes no softcap kwarg and its head_dim=256 kernel asserts
score_mod is None, so there is no kernel to route the cap through. The
FA4 call path in backends.py passes no softcap, so an FA4 selection with
a nonzero softcap silently dropped the cap instead of failing closed.
NVTE_FLASH_ATTN_V4 defaults to enabled, so this was reachable on SM100+
with flash-attn v4 installed and no context parallelism.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
flash-attn rejects a nonzero softcap combined with nonzero dropout at
dispatch: "Softcapping does not support dropout for now" in
csrc/flash_attn/flash_api.cpp, present in mha_fwd and mha_varlen_fwd
from v2.6.0 (the earliest version TE allows softcap on) onwards. Backend
selection did not model this, so a softcap + attention-dropout config
passed selection, routed to FA2, and crashed inside flash-attn.

Dropout only reaches the kernel while training, since backends.py passes
`self.attention_dropout if self.training else 0.0`, so the gate is on
`attention_dropout != 0.0 and is_training` to avoid blocking valid
inference configs. UnfusedDotProductAttention supports both softcap and
dropout and stays available, so this steers rather than hard-fails.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
@nvegesna-netizen
nvegesna-netizen force-pushed the nvegesna/gemma2-softcap-core branch from 82a2bf4 to 5ecabac Compare August 27, 2026 16:57
test_dot_product_attention forced is_training=False whenever FusedAttention could
not train a config, so that backends only available for inference could still be
compared. softcap always disables FusedAttention, so test_dpa_softcap silently
degraded to a forward-only comparison and the PR's backward-parity claim -- the FA2
softcap backward kernel included -- went untested.

Add fwd_only_without_fused_attn (default True, so every other caller is byte-for-byte
unchanged) and opt test_dpa_softcap out, which pairs FlashAttention against
UnfusedDotProductAttention with is_training=True and restores the dgrad comparison.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Two gaps remained after folding test_softcap.py into test_attention.py.

softcap=0.0 no-op: every model_configs_softcap entry uses a nonzero cap, so nothing
asserted the backward-compatibility claim. The half that the PR actually changed is
backend selection, and a filter that fired at 0.0 would silently remove FusedAttention
and FA4 from other tests rather than fail one. test_dpa_softcap_zero_backend_selection
asserts FusedAttention survives softcap=0.0 and is disabled by a nonzero cap.

Unfused coverage and tanh's nonlinear region: test_dpa_softcap needs two TE backends,
so it skips entirely without flash-attn even though UnfusedDotProductAttention now
implements softcap and is the reference for everything else. It also cannot detect a
dropped cap at all: 0.1 * randn inputs put the logits at O(1e-2), where the reference
output moves by 9e-9 at cap=50 and 2e-4 at cap=0.01. test_dpa_softcap_vs_reference
compares forward and dQ/dK/dV against a pure-PyTorch oracle one backend at a time, so
it runs with unfused alone, and uses randn inputs so the cap moves the output by O(1).
An assertion on that displacement keeps the test from going vacuous if the config drifts.

Signed-off-by: Nitin Vegesna <nvegesna@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2.20 community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants